
package com.ys.chatserver.http.file.u;

import cn.hutool.core.io.FileUtil;
import com.eva.framework.dto.DataFromServer;
import com.eva.framework.utils.LoggerFactory;
import com.ys.chatserver.BaseConf;
import com.ys.chatserver.http.file.dto.BigFileType;
import com.ys.chatserver.http.logic.LogicProcessor2;
import com.ys.chatserver.http.logic.filter.JwtHelper;
import com.ys.chatserver.http.logic.filter.JwtParam;
import com.ys.chatserver.tool.EmptyUtils;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.FileUtils;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.List;

/**
 * 通用大文件上传处理实现类（支持大文件断点分块上传）。
 * <p>
 * <b>提示：</b>大文件上传原理，详见 {@link BigUploaderRoot}中的详细说明。
 *
 * @author hst, Jack Jiang
 * @see BigFileType
 * @since 4.3
 */
public class DeviceFileUploader extends BigUploaderRoot {
    private static final long serialVersionUID = 1L;

    /**
     * 上传的文件要保存的目录（此目录末尾要带“/”哦）。
     */
    @Override
    public String getFileSaveDir() {
        return BigFileUploder.getCommonBigFileSaveDir();
    }

    /**
     * 支持的大文件类型
     */
    @Override
    protected int getFileType() {
        return BigFileType.FILE_TYPE_COMMON_BIG_FILE;
    }

    /**
     * 上传的通用大文件要保存的目录（此目录末尾要带“/”哦）。
     */
    public static String getCommonBigFileSaveDir() {
        return BaseConf.getInstance().getDIR_USER_BIGFILE_UPLOAD();
    }

    /**
     * 通用大文件最终保存绝对路径。
     */
    public static String getCommonBigFileSavePath(String fileName) {
        return BigFileUploder.getCommonBigFileSaveDir() + fileName;
    }

    /**
     * 返回通用大文件分块存放的临时目录。
     * <p>
     * 为了防止重复和混乱，分块文件是单独保存在形如“md5码_uid”的目录下。
     */
    public static String getChunckCommonBigFileDir(String fileMd5, String userUid) {
        return BigFileUploder.getCommonBigFileSaveDir() + fileMd5 + (userUid != null ? "_" + userUid : "") + "/";
    }

    @Override
    protected String getTAG() {
        return "大文件";
    }

    /**
     * <p>
     * Apache文件上传组件在解析上传数据中的每个字段内容时，需要临时保存解析出的数据，以
     * 便在后面进行数据的进一步处理（保存在磁盘特定位置或插入数据库）。因为Java虚拟机默
     * 认可以使用的内存空间是有限的，超出限制时将会抛出“java.lang.OutOfMemoryError”错
     * 误。如果上传的文件很大，例如800M的文件，在内存中将无法临时保存该文件内容，Apache
     * 文件上传组件转而采用临时文件来保存这些数据；但如果上传的文件很小，例如600个字节的
     * 文件，显然将其直接保存在内存中性能会更加好些。
     *
     * <p>
     * setSizeThreshold方法用于设置是否将上传文件已临时文件的形式保存在磁盘的临界值（以
     * 字节为单位的int值），如果从没有调用该方法设置此临界值，将会采用系统默认值10KB。对
     * 应的getSizeThreshold() 方法用来获取此临界值。
     */
    private static final int THRESHOLD_SIZE = 1024 * 200;      // 200KB
    private static final int MAX_FILE_SIZE = 1024 * 1024 * 200;// 200MB
    private static final int MAX_REQUEST_SIZE = MAX_FILE_SIZE;

    /**
     * Apache fileupload组件要使用的文件上传临时目录
     */
    private String repositoryDir;

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doPost(request, response);
    }

    @Override
    public void init(ServletConfig config) throws ServletException {
        repositoryDir = FileUtils.getTempDirectoryPath();
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        BufferedOutputStream outputStream = null;
        String token = request.getHeader("token");
        String groupId = request.getHeader("groupId");
        JwtParam jwtParam = JwtHelper.parseJwt(token);
        String devId = jwtParam.getUserId();
        // 客户端上传的当前文件分块编号（起始编号是1）
        Integer schunk = null;
        // 文件总分块数
        Integer schunks = null;
        // 客户端传上来的文件名（文件的真正名字）
        String name = null;
        // 总文件的大小（而不是本次上传的“块”）
        String totalLength = null;
        // 总文件的MD5码（而不是本次上传的“块”）
        String totalFileMd5 = null;
        // 客户传上来的身份认证token，暂时为保留字段，留作日后作上传安全策略使用
        // 上传者的uid
        String user_uid = null;
        String uploadDir = BaseConf.getInstance().getDIR_DEVICE_FILE_UPLOAD() + devId;
        if (!FileUtil.exist(uploadDir)) {
            File up = new File(uploadDir);
            Boolean res = up.mkdirs();
        }
        try {
            response.setCharacterEncoding("UTF-8");
            DiskFileItemFactory factory = new DiskFileItemFactory();
            factory.setSizeThreshold(THRESHOLD_SIZE);
            factory.setRepository(new File(repositoryDir));
            ServletFileUpload upload = new ServletFileUpload(factory);
            upload.setSizeMax(MAX_FILE_SIZE);
            List<FileItem> items = upload.parseRequest(request);
            // 服务端真正保存的文件名
            String totalFileNameSavedOnServer = null;
            // 分块文件的临时保存位置
            File chunckSavedDir = null;

            for (FileItem item : items) {
                // 如果是文件类型
                if (!item.isFormField()) {
                    totalFileMd5 = totalFileMd5.toLowerCase();
                    // 要保存到碰盘上的真正文件名（目前是md5码作为文件名，防止上传重名的情况）
                    totalFileNameSavedOnServer = totalFileMd5;//name;
                    chunckSavedDir = new File(getChunckFileDir(totalFileMd5, user_uid));
                    if (!chunckSavedDir.exists()) {
                        chunckSavedDir.mkdirs();
                    }
                    if (totalFileNameSavedOnServer != null) {
                        String chunkFileName = schunk + "_" + totalFileNameSavedOnServer;
                        File chunkSavedFile = new File(chunckSavedDir, chunkFileName);
                        item.write(chunkSavedFile);
                    }
                }
                // 普通的属性字段
                else {
                    if (item.getFieldName().equals("chunk"))
                        schunk = Integer.parseInt(item.getString());
                    else if (item.getFieldName().equals("chunks"))
                        schunks = Integer.parseInt(item.getString());
                    else if (item.getFieldName().equals("name"))
                        name = item.getString();
                    else if (item.getFieldName().equals("totalLength"))
                        totalLength = item.getString();
                    else if (item.getFieldName().equals("totalFileMd5")) {
                        totalFileMd5 = item.getString();
                        if (totalFileMd5 != null)
                            totalFileMd5 = totalFileMd5.toLowerCase();
                    } else if (item.getFieldName().equals("user_uid"))
                        user_uid = item.getString();
                }
            }

            if (schunk != null && schunk.intValue() == schunks.intValue()) {
                File totalFileNameSavedOnServerFile = new File(uploadDir, name);
                outputStream = new BufferedOutputStream(new FileOutputStream(totalFileNameSavedOnServerFile));
                for (int i = 1; i <= schunks; i++) {
                    File tempFile = new File(chunckSavedDir, i + "_" + totalFileNameSavedOnServer);
                    byte[] bytes = FileUtils.readFileToByteArray(tempFile);
                    outputStream.write(bytes);
                    outputStream.flush();
                    tempFile.delete();
                }
                outputStream.flush();
                afterTotalFileUploadComplete(devId, name, totalLength,groupId);
                try {
                    if (chunckSavedDir.exists()) {
                        chunckSavedDir.delete();
                    }
                } catch (Exception e) {
                    LoggerFactory.getLog().error(e.getMessage());
                }
            }
            DataFromServer fromServer = new DataFromServer();
            fromServer.setSuccess(true);
            fromServer.setReturnValue("1");
            sendToClient(response, fromServer);
        } catch (Exception e) {
            String logStr = "[其它参数：schunk=" + schunk + ", schunks=" + schunks
                    + "newFileName=" + name + ", totalLength=" + totalLength + ", totalFileMd5=" + totalFileMd5
                    + ", token=" + token + ", user_uid=" + user_uid + "]";
            LoggerFactory.getLog().error("[HTTP大文件上传] 出错了，客户端上传的各参数：" + logStr, e);
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            response.getWriter().write(logStr);
        } finally {
            try {
                if (outputStream != null)
                    outputStream.close();
            } catch (Exception e) {
                LoggerFactory.getLog().error(e.getMessage());
            }
        }
    }

    private void afterTotalFileUploadComplete(String devId, String filename, String size, String groupId) throws Exception {
        if (EmptyUtils.isNotEmpty(devId) && EmptyUtils.isNotEmpty(filename)) {
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String time = simpleDateFormat.format(new Date());
            String buffer = "insert into t_cloud(g_id,create_uid,create_time,file_name,file_type,store_type,file_size)values('" +
                    groupId + "','" + devId + "','" + time + "','" + filename + "'," + 1 + "," + 2 + ",'" + size + "')";
            LogicProcessor2.db.update(Collections.singletonList(buffer), false);
        }
    }
}
